﻿namespace WP7Contrib.Communications
{
    using System.IO;
    using System.Net.Browser;
    using System.Reactive.Concurrency;
    using System.Reactive.Linq;
    using System.Text;
    using System.Net;
    using System;

    public class RestClient
    {
        protected enum HttpMethod
        {
            GET,
            POST,
            PUT,
            DELETE
        }

        public RestClient() : this(new JsonContractSerializer()) { }

        public RestClient(ISerializer serializer) : this((Uri)null, serializer, serializer) { }

        public RestClient(ISerializer requestSerializer, ISerializer responseSerializer) : this((Uri)null, requestSerializer, responseSerializer) { }

        public RestClient(string baseUrl, ISerializer serializer) : this(baseUrl, serializer, serializer) { }

        public RestClient(string baseUrl, ISerializer requestSerializer, ISerializer responseSerializer) : this(new Uri(baseUrl), requestSerializer, responseSerializer) { }

        public RestClient(Uri baseUrl, ISerializer requestSerializer, ISerializer responseSerializer)
        {
            BaseUrl = baseUrl;
            RequestSerializer = requestSerializer;
            ResponseSerializer = responseSerializer;
            this.CookieContainer = new CookieContainer();
        }

        public Uri BaseUrl { get; set; }

        public ISerializer RequestSerializer { get; private set; }

        public ISerializer ResponseSerializer { get; private set; }

        private CookieContainer CookieContainer { get; set; }

        public virtual IObservable<RestAsyncResponse<T>> Get<T>(string url, NetworkCredential credentials = null) where T : class
        {
            return ExecuteRequest<T>(url, HttpMethod.GET, null, credentials);
        }

        public virtual IObservable<RestAsyncResponse<T>> Post<T>(string url, object data = null, NetworkCredential credentials = null) where T : class
        {
            return ExecuteRequest<T>(url, HttpMethod.POST, data, credentials);
        }

        public virtual IObservable<RestAsyncResponse<T>> Put<T>(string url, object data = null, NetworkCredential credentials = null) where T : class
        {
            return ExecuteRequest<T>(url, HttpMethod.PUT, data, credentials);
        }

        public virtual IObservable<RestAsyncResponse<T>> Delete<T>(string url, NetworkCredential credentials = null) where T : class
        {
            return ExecuteRequest<T>(url, HttpMethod.DELETE, null, credentials);
        }


        protected virtual IObservable<RestAsyncResponse<T>> ExecuteRequest<T>(string url, HttpMethod method, object data, NetworkCredential credentials) where T : class
        {
            var uri = new Uri(BaseUrl, url);

            var request = (HttpWebRequest)WebRequestCreator.ClientHttp.Create(uri);
            request.Method = method.ToString();
            request.Accept = ResponseSerializer.ContentType;
            request.CookieContainer = this.CookieContainer;
            SetBasicAuth(request, credentials);

            if (data == null || string.IsNullOrEmpty(data as string))
            {
                return ExecuteRequestWithNoBody<T>(request);
            }
            else
            {
                return ExecuteRequestWithBody<T>(request, data);
            }
        }

        private IObservable<RestAsyncResponse<T>> ExecuteRequestWithBody<T>(HttpWebRequest request, object data) where T : class
        {
            var body = Serialize(data);
            request.ContentType = RequestSerializer.ContentType;

            return Observable.FromAsyncPattern<Stream>(request.BeginGetRequestStream, request.EndGetRequestStream)()
                .ObserveOn(Scheduler.ThreadPool)
                .SelectMany(stream =>
                {
                    using (stream)
                    {
                        stream.Write(body, 0, body.Length);
                        stream.Close();
                    }

                    return Observable.FromAsyncPattern<WebResponse>(request.BeginGetResponse, request.EndGetResponse)()
                        .ObserveOn(Scheduler.ThreadPool);

                },
                (stream, response) => this.ProcessResponse<T>((HttpWebResponse)response))
                    .ObserveOn(Scheduler.ThreadPool);

        }

        private IObservable<RestAsyncResponse<T>> ExecuteRequestWithNoBody<T>(HttpWebRequest request) where T : class
        {
            return Observable.FromAsyncPattern(request.BeginGetResponse, asyncResult => this.ProcessResponse<T>(asyncResult, request))()
                .ObserveOn(Scheduler.ThreadPool)
                .Select(response => response);
        }

        private static void SetBasicAuth(HttpWebRequest request, NetworkCredential credentials)
        {
            // Since WP7 is based on Silverlight 3, HttpWebRequest.Credentials is not yet supported.
            // Authorization needs to be done by hand.
            if (credentials != null)
            {
                var authString = string.Format("{0}:{1}", credentials.UserName, credentials.Password);
                var utf8bytes = Encoding.UTF8.GetBytes(authString);
                authString = string.Format("Basic {0}", Convert.ToBase64String(utf8bytes));
                request.Headers[HttpRequestHeader.Authorization] = authString;
            }
        }

        private RestAsyncResponse<T> ProcessResponse<T>(IAsyncResult asyncResult, HttpWebRequest request) where T : class
        {
            try
            {
                using (var response = (HttpWebResponse)request.EndGetResponse(asyncResult))
                {
                    return ProcessResponse<T>(response);
                }
            }
            catch (WebException ex)
            {
                using (var response = (HttpWebResponse)ex.Response)
                {
                    System.Diagnostics.Debug.WriteLine(response.StatusDescription);
                    return new RestAsyncResponse<T>(response.StatusCode, ex, default(T));
                }
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine(ex.ToString());
                return new RestAsyncResponse<T>(0, ex, default(T));
            }
        }

        private RestAsyncResponse<T> ProcessResponse<T>(HttpWebResponse response) where T : class
        {
            try
            {
                using (var stream = response.GetResponseStream())
                {
                    var result = Deserialize<T>(stream);
                    return new RestAsyncResponse<T>(response.StatusCode, null, result);
                }
            }
            catch (Exception ex)
            {
                return new RestAsyncResponse<T>(response.StatusCode, ex, default(T));
            }
        }

        protected byte[] Serialize(object data)
        {
            byte[] result = null;

            if (data != null)
            {
                if (data is string)
                {
                    //bypass serialization if data is already a string
                    result = Encoding.UTF8.GetBytes(data as string);
                }
                else if (data is Stream)
                {
                    //bypass serialization if data is a Stream.
                    var stream = data as Stream;
                    stream.Position = 0;
                    result = new byte[stream.Length];
                    stream.Read(result, 0, result.Length);
                }
                else
                {
                    using (var stream = RequestSerializer.Serialize(data))
                    {
                        result = new byte[stream.Length];
                        stream.Read(result, 0, result.Length);
                    }
                }
            }

            return result;
        }

        protected T Deserialize<T>(Stream stream) where T : class
        {
            T result = default(T);

            if (stream != null && stream.Length > 0)
            {
                stream.Position = 0;
                if (typeof(T) == typeof(string))
                {
                    // Bypass deserialization for type string. Simply returns the response body.
                    var sr = new StreamReader(stream);
                    result = sr.ReadToEnd() as T;
                }
                else if (typeof(T) == typeof(Stream))
                {
                    // Bypass deserialization for type Stream. Simply returns a clone of the response stream.
                    // The response stream needs to be cloned because it would be disposed before returning.
                    byte[] content = new byte[stream.Length];
                    stream.Read(content, 0, content.Length);
                    var ms = new MemoryStream(content)
                    {
                        Position = 0
                    };
                    return ms as T;
                }
                else
                {
                    result = ResponseSerializer.Deserialize<T>(stream);
                }
            }

            return result;
        }
    }
}